/*
 * Copyright (c) 2011, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.wso2.carbon.registry.lifecycle.accenture.sample.executors;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMFactory;
import org.apache.axiom.om.impl.builder.StAXOMBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.governance.api.services.ServiceManager;
import org.wso2.carbon.governance.api.services.dataobjects.Service;
import org.wso2.carbon.governance.registry.extensions.interfaces.Execution;
import org.wso2.carbon.registry.core.Association;
import org.wso2.carbon.registry.core.Registry;
import org.wso2.carbon.registry.core.Resource;
import org.wso2.carbon.registry.core.ResourcePath;
import org.wso2.carbon.registry.core.exceptions.RegistryException;
import org.wso2.carbon.registry.core.jdbc.handlers.RequestContext;
import org.wso2.carbon.registry.extensions.utils.CommonConstants;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import java.io.ByteArrayInputStream;
import java.io.StringReader;
import java.util.*;

import static org.apache.axiom.om.util.AXIOMUtil.stringToOM;
import static org.wso2.carbon.registry.extensions.utils.CommonUtil.setServiceVersion;
import static org.wso2.carbon.registry.extensions.utils.CommonUtil.setWSDLURL;

public class AutomatedServiceVersionExecutor implements Execution {

    private static final Log log = LogFactory.getLog(AutomatedServiceVersionExecutor.class);

    private static String SERVICE_MEDIA_TYPE = "application/vnd.wso2-service+xml";
    private static String ENDPOINT_MEDIA_TYPE = "application/vnd.wso2.endpoint";
    private static String WSDL_MEDIA_TYPE = "application/wsdl+xml";
    private final String RESOURCE_VERSION = "{@version}";
    private final String RESOURCE_NAME = "{@resourceName}";
    private final String RESOURCE_PATH = "{@resourcePath}";
    private static final String REGISTRY_LC_NAME = "registry.LC.name";

    private List<String> otherDependencyList = new ArrayList<String>();
    private Map parameterMap = new HashMap();

    @Override
    public void init(Map parameterMap) {
        //To change body of implemented methods use File | Settings | File Templates.
        this.parameterMap = parameterMap;

        if(parameterMap.get("service.mediatype")!= null){
            SERVICE_MEDIA_TYPE = parameterMap.get("service.mediatype").toString();
        }
        if(parameterMap.get("wsdl.mediatype")!= null){
            WSDL_MEDIA_TYPE = parameterMap.get("wsdl.mediatype").toString();
        }
        if(parameterMap.get("endpoint.mediatype")!= null){
            ENDPOINT_MEDIA_TYPE = parameterMap.get("endpoint.mediatype").toString();
        }

    }

    @Override
    public boolean execute(RequestContext requestContext, String currentState, String targetState) {

        //getting the necessary values from the request context
        Resource resource = requestContext.getResource();
        Registry registry = requestContext.getRegistry();
        String resourcePath = requestContext.getResourcePath().getPath();

        if ((resource instanceof org.wso2.carbon.registry.api.Collection)) {
            return true;
        }

        //Getting the target environment and the current environment from the parameter map.
        String targetEnvironment = (String) parameterMap.get("targetEnvironment");
        String currentEnvironment = (String) parameterMap.get("currentEnvironment");

        if ((targetEnvironment == null || currentEnvironment == null) || (currentEnvironment.isEmpty()
                || targetEnvironment.isEmpty())) {
            log.warn("Current environment and the Target environment has not been defined to the state");
            /*Here we are returning true because the executor has been configured incorrectly
             *We do NOT consider that as a execution failure
             *Hence returning true here */
            return true;
        }

        String newVersion="";

        Set parameterMapKeySet = (Set) requestContext.getProperty("parameterNames");
        if (parameterMapKeySet != null) {
            for (Object entry : parameterMapKeySet) {
                String key = (String) entry;
                newVersion = (String) requestContext.getProperty(key);
            }
        }
//         Here we are populating the parameter map that was given from the UI
//
//        if (!populateParameterMap(requestContext, currentParameterMap)) {
//            log.error("Failed to populate the parameter map");
//            return false;
//        }
        //Here we are generating the currentParameterMap

        //Now we are going to get the list of parameters from the context and add it to a map
        Map<String, String> currentParameterMap = new HashMap<String, String>();
        String currentResourcePath = resourcePath;

        currentParameterMap.put(resourcePath, (newVersion=="")?getIncrementVersion(getResourceVersion(requestContext)):newVersion);
        Map<String, String> oldPathNewPathMap = new HashMap<String, String>();
        try {
            Resource newResource = registry.newResource();

            // This loop is there to reformat the paths with the new versions.
            for (Map.Entry<String, String> keyValueSet : currentParameterMap.entrySet()) {
                String path = keyValueSet.getKey();
                path = reformatPath(path, currentEnvironment, targetEnvironment);

             //This condition is there to check whether we need to move the resources
             //The executor will not execute beyond this point, to all the resources that are not under
             // the given environment prefix

                if (path.equals(keyValueSet.getKey())) {
                    log.info("Resource " + path + " is not in the given environment");
                    otherDependencyList.add(path);
                    continue;
                }

                if (path.contains(RESOURCE_VERSION)) {
                    path = path.replace(RESOURCE_VERSION, keyValueSet.getValue());
                }
                oldPathNewPathMap.put(keyValueSet.getKey(), path);
                try {
                    if (!keyValueSet.getKey().equals(resourcePath)) {
                        registry.copy(keyValueSet.getKey(), path);
                    } else {
                        String newContent = new String((byte[]) resource.getContent());
                        newResource.setContent(newContent.getBytes());
                    }
                } catch (RegistryException e) {
                    log.error("Failed to copy the resource from " + keyValueSet.getKey() + " to " + path, e);
                    return false;
                }
            }

            for (String otherDependency : otherDependencyList) {
                currentParameterMap.remove(otherDependency);
            }
             /*
              * Once the paths are updated with the new versions we do through the service resource and update the
              * content of the service resource with the new service version, wsdl path.
              */
            for (Map.Entry<String, String> keyValueSet : currentParameterMap.entrySet()) {
                if (registry.resourceExists(keyValueSet.getKey())) {
                    Resource tempResource = registry.get(keyValueSet.getKey());
                    if (tempResource.getMediaType().equals(SERVICE_MEDIA_TYPE)) {
                        OMElement newContent = stringToOM(new String((byte[]) newResource.getContent()));
                        setServiceVersion(newContent, currentParameterMap.get(keyValueSet.getKey()));
                        newResource.setContent(newContent.toString().getBytes());
                        resourcePath = oldPathNewPathMap.get(keyValueSet.getKey());
                        continue;
                    }
                    if (tempResource.getMediaType().equals(WSDL_MEDIA_TYPE)) {
                        OMElement newContent = stringToOM(new String((byte[]) newResource.getContent()));
                        setWSDLURL(newContent, oldPathNewPathMap.get(keyValueSet.getKey()));
                        newResource.setContent(newContent.toString().getBytes());
                        continue;
                    }
                    if (tempResource.getMediaType().equals(ENDPOINT_MEDIA_TYPE)) {
                        OMElement newContent = stringToOM(new String((byte[]) newResource.getContent()));
                        removeEndPointValues(newContent, oldPathNewPathMap.get(keyValueSet.getKey()), keyValueSet.getKey());
                        newResource.setContent(newContent.toString().getBytes());
                    }
                } else {
                    log.error("Wrong dependency path given");
                    return false;
                }

            }

             // Adding the new service with the updated content.

            if (!SERVICE_MEDIA_TYPE.equals(resource.getMediaType())) {
                log.warn("Media type of the resource does not match service media type");
            } else {
                try {
                    ServiceManager manager = new ServiceManager(registry);
                    XMLStreamReader reader = XMLInputFactory.newInstance().createXMLStreamReader(
                            new ByteArrayInputStream((byte[]) newResource.getContent()));
                    StAXOMBuilder builder = new StAXOMBuilder(reader);
                    OMElement serviceElement = builder.getDocumentElement();
                    serviceElement.build();
                    OMFactory fac = OMAbstractFactory.getOMFactory();
                    //adding required fields at the top of the xml which will help to easily read in service side
                    Iterator it = serviceElement.getChildrenWithLocalName("newServicePath");
                    if (it.hasNext()) {
                        OMElement next = (OMElement) it.next();
                        next.setText(resourcePath);
                    } else {
                        OMElement operation = fac.createOMElement("newServicePath", serviceElement.getNamespace(), serviceElement);
                        operation.setText(resourcePath);
                    }
                    //this is here to override the default path
                    serviceElement.build();

                    Service service = manager.newService(serviceElement);
                    manager.addService(service);
                } catch (XMLStreamException e) {
                    log.warn("Failed to read the content of the resource");
                }
            }

            // Associating the new resource with the LC
            String aspectName = resource.getProperty(REGISTRY_LC_NAME);
            String stateProperty = "registry.lifecycle." + aspectName + ".state";
            registry.associateAspect(oldPathNewPathMap.get(currentResourcePath), aspectName);

           // This operation is there to update the resource
            newResource = registry.get(oldPathNewPathMap.get(currentResourcePath));
            Properties properties = (Properties) resource.getProperties().clone();
            newResource.setProperties(properties);

            newResource.setProperty(stateProperty, targetState.replace(".", " "));

            requestContext.setResource(newResource);
            requestContext.setOldResource(resource);
            requestContext.setResourcePath(new ResourcePath(oldPathNewPathMap.get(currentResourcePath)));

            makeAssociations(requestContext, currentParameterMap, oldPathNewPathMap);
            makeOtherAssociations(requestContext, oldPathNewPathMap, otherDependencyList);
            registry.put(resourcePath, newResource);
        } catch (RegistryException e) {
            log.error("Failed to perform registry operation", e);
        } catch (XMLStreamException e) {
            log.error("Error occurred while executing the ServiceVersionExecutor", e);
        }
        return true;
    }

    private void makeAssociations(RequestContext requestContext, Map<String, String> parameterMap
            , Map<String, String> oldPathNewPathMap) throws RegistryException {

        Registry registry = requestContext.getRegistry();

        for (Map.Entry<String, String> keyValueSet : parameterMap.entrySet()) {
            Association[] associations = registry.getAllAssociations(keyValueSet.getKey());
            for (Association association : associations) {
                if (association.getDestinationPath().equals(keyValueSet.getKey())) {
                    registry.removeAssociation(association.getSourcePath(), oldPathNewPathMap.get(association.getDestinationPath()),
                            association.getAssociationType());
                }
                if (association.getSourcePath().equals(keyValueSet.getKey())) {
                    registry.removeAssociation(oldPathNewPathMap.get(association.getSourcePath()),
                            association.getDestinationPath(),
                            association.getAssociationType());
                }
                if (oldPathNewPathMap.containsKey(association.getDestinationPath())
                        && oldPathNewPathMap.containsKey(association.getSourcePath())) {
                    registry.addAssociation(oldPathNewPathMap.get(association.getSourcePath())
                            , oldPathNewPathMap.get(association.getDestinationPath())
                            , association.getAssociationType());
                }
            }
        }
    }

    private void makeOtherAssociations(RequestContext requestContext, Map<String, String> oldPathNewPathMap
            , List<String> otherDependencies) throws RegistryException {

        Registry registry = requestContext.getRegistry();
        for (Map.Entry<String, String> entry : oldPathNewPathMap.entrySet()) {
            Association[] associations = registry.getAllAssociations(entry.getKey());
            for (Association association : associations) {
                for (String dependency : otherDependencies) {
                    if (association.getDestinationPath().equals(dependency)) {
                        registry.addAssociation(entry.getValue(), dependency, association.getAssociationType());
                    }
                    if (association.getSourcePath().equals(dependency)) {
                        registry.addAssociation(dependency, entry.getValue(), association.getAssociationType());
                    }
                }
            }
        }
    }
//   private boolean populateParameterMap(RequestContext requestContext, Map<String, String> currentParameterMap) {
//        Set parameterMapKeySet = (Set) requestContext.getProperty("parameterNames");
//        if (parameterMapKeySet == null) {
//            return false;
//        }
//        for (Object entry : parameterMapKeySet) {
//            String key = (String) entry;
//            if (!key.equals("preserveOriginal")) {
//                currentParameterMap.put(key, (String) requestContext.getProperty(key));
//            }
//        }
//        return true;
//    }

    private String reformatPath(String path, String currentExpression, String targetExpression) {
        String returnPath = targetExpression;

        if (currentExpression.equals(targetExpression)) {
            return path;
        }
        boolean isResourceName = currentExpression.contains(RESOURCE_NAME);
        boolean isResourcePath = currentExpression.contains(RESOURCE_PATH);
        boolean isResourceVersion = currentExpression.contains(RESOURCE_VERSION);

        int pathIndex = currentExpression.indexOf(RESOURCE_PATH);
        String pathPrefix = currentExpression.substring(0, pathIndex);

        if (path.startsWith(pathPrefix)) {
            if (isResourceName) {
                returnPath = returnPath.replace(RESOURCE_NAME, path.substring(path.lastIndexOf("/") + 1));
                path = path.substring(0, path.lastIndexOf("/"));
            }if (isResourceVersion) {
                path = path.substring(0, path.lastIndexOf("/"));
            }if (isResourcePath) {
                String resourcePath = path.substring(pathIndex);
                returnPath = returnPath.replace(RESOURCE_PATH, resourcePath);
            }
        } else {
            return path;
        }
        return returnPath;
    }
    //    From this method we are removing the endpoint values from the content since they are added back again when associating with the service
    public static void removeEndPointValues(OMElement element, String newValue, String oldValue) {
        OMElement endPointRoot = element.getFirstChildWithName(new QName(CommonConstants.SERVICE_ELEMENT_NAMESPACE,"endpoints"));
        if (endPointRoot != null) {
            Iterator endpointIterator = endPointRoot.getChildElements();
            while (endpointIterator.hasNext()) {
                OMElement endPoint = (OMElement) endpointIterator.next();
                if(endPoint.getText().equals(oldValue)){
                    endPoint.detach();
                    break;
                }
            }
            element.build();
        }
    }

    /**
     * used to get OMElement  of current service
     * @param content element of config
     * @return config OMElement
     * @throws Exception if OMElement creation failed
     */
    private static OMElement getUIConfiguration(String content) throws Exception {
        OMElement omElement = null;
        try {
            XMLStreamReader reader = XMLInputFactory.newInstance().createXMLStreamReader(new StringReader(content));
            StAXOMBuilder builder = new StAXOMBuilder(reader);
            omElement = builder.getDocumentElement();
        } catch (XMLStreamException e) {
            log.error("Unable to parse the UI configuration.", e);
        }
        return omElement;
    }

    /**
     * used to get value  of version tag
     * @param omElement  version OMElement
     * @return if failed to get version
     */
    private static String getVersion(OMElement omElement) {
        String version = null;
        OMElement overviewOmElement = omElement.getFirstChildWithName(new QName("http://www.wso2.org/governance/metadata", "overview"));
        if (overviewOmElement != null) {
            Iterator versionOMElements = overviewOmElement.getChildrenWithName(new QName("version"));
            while (versionOMElements.hasNext()) {
                OMElement versionOmElement = (OMElement) versionOMElements.next();
                version = versionOmElement.getText();
            }
        }
        return version;

    }

    /**
     * used to get version of service which we going to promote
     * @param requestContext RequestContext
     * @return  version
     */
    private static String getResourceVersion(RequestContext requestContext) {
        String version = null;
        String path = requestContext.getResource().getPath();
        try {
            String serviceResource;
            Object content = requestContext.getRegistry().get(path).getContent();
            if (content instanceof String) {
                serviceResource = (String) content;
            } else {
                serviceResource = new String((byte[]) content);
            }
            version = getVersion(getUIConfiguration(serviceResource));

        } catch (RegistryException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return version;
    }

    /**
     * used to compute version number of promoted service
     * @param version current version
     * @return new version
     */
    private static String getIncrementVersion(String version) {
        String major = version.split("\\.")[0];
        String minor = version.split("\\.")[1];
        String patch = version.split("\\.")[2];

        Integer majorInt = Integer.parseInt(major);
        ++majorInt;
        String majorStr = String.valueOf(majorInt);

        return majorStr + "." + minor + "." + patch;
    }
}
